Skip to main content

Best Practices of FastAPI-Python

· 9 min read

FastAPI is a modern, fast (high-performance) Python framework for building APIs. Its ease of use and powerful features make it an excellent choice for developers. However, to maximize its potential, it’s crucial to follow best practices. This blog outlines some essential tips to ensure your FastAPI application is secure, scalable, and maintainable.

1. Setting Up Your Project Structure

A well-organized project structure makes your application scalable and easier to maintain. Below is an example:

project-name/
├── app/
│ ├── main.py # Entry point for FastAPI app
│ ├── config.py # Configuration settings
│ ├── routes/ # All route definitions
│ │ ├── __init__.py # Makes routes a module
│ │ ├── user.py # User-related endpoints
│ │ ├── auth.py # Authentication-related endpoints
│ ├── models/ # Database models
│ │ ├── __init__.py # Makes models a module
│ │ ├── user.py # User model
│ ├── services/ # Business logic
│ │ ├── __init__.py # Makes services a module
│ │ ├── auth_service.py # Authentication logic
│ ├── static/ # Static files (CSS, JS, images)
│ ├── templates/ # HTML templates
│ ├── utils/ # Utility functions
│ │ ├── __init__.py # Makes utils a module
│ │ ├── helpers.py # Helper functions
├── tests/ # Unit and integration tests
├── .env # Environment variables
├── pyproject.toml # Poetry configuration
├── poetry.lock # lockfile for consistent builds
├── README.md # Documentation

2. Use Poetry.

Poetry is a dependency management tool that simplifies working with Python projects. Here’s why it’s a great fit for your FastAPI applications:

  • Automatic Dependency Resolution: Poetry resolves dependencies and ensures there are no conflicts.
  • Isolated Environments: Automatically creates and manages a virtual environment for your project.
  • Lockfile Generation: Generates a poetry.lock file that ensures consistent dependency installations across different environments.
  • Ease of Use: Handles versioning, packaging, and publishing with simple commands.

3. Environment Variables

Proper configuration of environment variables is critical. Store sensitive information like API keys, database credentials, and secrets in a .env file. Use a library like python-dotenv to load these variables into your application:

poetry add python-dotenv

Example .env file:

DATABASE_URL=postgresql://user:password@localhost/db_name
SECRET_KEY=your_secret_key
AZURE_REGION=your_azure_region
AZURE_SPEECH_KEY=your_azure_key

4. Asynchronous Programming

FastAPI is designed for asynchronous programming, making it perfect for non-blocking I/O operations. Always use async def for endpoints and services where applicable.

Example:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
name: str
description: str

@app.post("/items/")
async def create_item(item: Item):
return {"message": f"Item {item.name} created successfully!"}

Leverage FastAPI’s Asynchronous Capabilities for request handling:

FastAPI is built on asynchronous programming principles. Use async def for non-blocking I/O operations such as: HTTP requests Database queries File I/O

Asynchronous HTTP Requests:

Example:

import httpx

@app.get("/external-api")
async def call_external_api():
async with httpx.AsyncClient() as client:
response = await client.get("https://api.example.com/data")
return response.json()

Consequences of Not Using asynchronous programming in FastAPI

  1. Blocking I/O Operations When synchronous endpoints are used (i.e., with def instead of async def), any I/O operation like a database query or an external API call will block the entire thread until it completes. This means that no other requests can be processed during this time, leading to a bottleneck.

    Example of a Blocking Call:

    import requests

    @app.get("/external-api")
    def call_external_api():
    response = requests.get("https://api.example.com/data")
    return response.json()

    In the above example, while the requests.get call is waiting for a response, no other requests can be handled by that thread. If multiple clients make similar requests, the application may become unresponsive.

  2. Inefficient Resource Utilization FastAPI’s asynchronous nature is optimized for modern architectures. By avoiding async def, your application loses the ability to utilize Python’s asyncio event loop, resulting in inefficient use of CPU and memory resources.

    For example, instead of handling thousands of requests concurrently, the application might struggle to handle just a few hundred due to threads being locked by blocking calls.

  3. Poor Scalability In high-traffic scenarios, synchronous programming severely limits scalability. With synchronous endpoints, each request ties up a thread, and the server can quickly run out of threads to handle additional requests, leading to delays or dropped connections.

  4. Inconsistent Performance Synchronous endpoints can cause unpredictable delays for clients, especially if one slow request blocks the thread and prevents other requests from being processed. This inconsistency can degrade the overall user experience.


5. Exception Handling

FastAPI provides custom exception handling capabilities to manage errors gracefully.

Example of Custom Exception Handling:

from fastapi import HTTPException

@app.get("/resource/{item_id}")
async def read_item(item_id: int):
if item_id < 1:
raise HTTPException(status_code=400, detail="Invalid item ID")
return {"item_id": item_id}

You can also create a global exception handler:

from fastapi.responses import JSONResponse

@app.exception_handler(Exception)
async def global_exception_handler(request, exc):
return JSONResponse(
status_code=500,
content={"message": "An unexpected error occurred."},
)

6. Logging

Use Python’s built-in logging module for structured logging. Logging provides visibility into the inner workings of your application, making it easier to monitor, troubleshoot, and maintain your code.

Example:

import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("name")

@app.on_event("startup")
async def startup_event():
logger.info("Application startup complete.")

Output:

INFO:name:Application startup complete.

7. Loading Data During Server Startup

FastAPI provides the lifespan context manager to define tasks during the startup and shutdown phases of your application. For instance, you can load data, establish database connections, or initialize services during the startup phase.

Example of Using lifespan:

from fastapi import FastAPI
from fastapi.lifespan import Lifespan

app = FastAPI(lifespan=Lifespan())

@app.on_event("startup")
async def startup_event():
print("Loading data during startup...")
# Example: Load configuration data from Azure Blob Storage
app.state.prompt = await get_prompt_from_blob()
print("Data loaded successfully!")

@app.on_event("shutdown")
async def shutdown_event():
print("Shutting down application...")
# Example: Cleanup resources

async def get_prompt_from_blob():
# Simulate fetching data from Azure Blob Storage
return "Prompt loaded from Blob Storage."

8. Secure Your Application

  • Use HTTPS in Production: Ensure your app runs over HTTPS using tools like Nginx or a cloud provider.
  • Avoid Hardcoding Secrets: Use environment variables for sensitive data.

Authentication and Authorization

For any web application, authentication and authorization are essential to ensure only authorized users can access certain resources.

  • OAuth2 with Password Flow: FastAPI supports OAuth2, which is often used in applications to authenticate users. You can integrate - OAuth2 providers like Google, GitHub, or Twitter.

  • JWT (JSON Web Tokens): Use JWT tokens for stateless authentication. After logging in, the user receives a token that should be included in the headers for future requests.

from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from typing import Optional

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

def get_current_user(token: str = Depends(oauth2_scheme)):
# Validate the token and return the user
user = decode_jwt(token) # decode_jwt is a function you would define
if not user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
return user

9. Background Tasks

Offload long-running tasks to background processes using FastAPI’s BackgroundTasks.

Example:

from fastapi import BackgroundTasks

async def send_email(email: str):
# Simulate a long-running email operation
await asyncio.sleep(5)
print(f"Email sent to {email}")

@app.post("/send-email/")
async def trigger_email(background_tasks: BackgroundTasks, email: str):
background_tasks.add_task(send_email, email)
return {"message": "Email will be sent soon"}

10. Serving Static Files

Use fastapi.staticfiles to serve static files like CSS, JavaScript, or images.

Example:

from fastapi.staticfiles import StaticFiles

app.mount("/static", StaticFiles(directory="app/static"), name="static")

Access static files via /static/<file_name>.


11. Pydantic: Simplifying Input and Output Validation in Python

Pydantic is a powerful library for data validation and settings management in Python. It is especially useful for applications where structured data is required, such as in web APIs, configuration files, and more. By utilizing Python's type annotations, Pydantic allows developers to define models that handle input and output validation seamlessly.

Input Validation with Pydantic

When working with user input or API requests, it's crucial to validate that the data adheres to expected types and constraints. Pydantic helps by defining models with specific types and validation rules.

For example:

from pydantic import BaseModel, Field

class User(BaseModel):
name: str
age: int
email: str = Field(..., regex=r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')

# Example usage
user_data = {
"name": "John Doe",
"age": 30,
"email": "[email protected]"
}

user = User(**user_data)
print(user)

In this example, Pydantic ensures that the name is a string, age is an integer, and the email matches the provided regular expression pattern. If any of these conditions aren't met, Pydantic raises a validation error, providing detailed feedback.

Output Validation with Pydantic

Pydantic not only helps validate incoming data but also ensures that the output is structured correctly. After performing operations (like data transformations or API responses), you can define Pydantic models to enforce the correct structure for the output.

class ResponseModel(BaseModel):
status: str
data: dict

# Example usage
response = ResponseModel(status="success", data={"message": "Operation completed successfully"})
print(response.json())

Here, ResponseModel validates that the status is a string and data is a dictionary before the response is returned.

Benefits of Using Pydantic

  • Type Safety: Pydantic enforces strict typing, reducing the risk of bugs caused by incorrect data types.
  • Automatic Data Validation: You can define complex validation rules using built-in validators, reducing manual error-checking.
  • Error Handling: Validation errors are raised with detailed messages, making it easier to debug and correct input data.
  • Fast: Pydantic is optimized for performance, ensuring that validation operations are efficient even with large datasets.

12. Practices When Using Global Variables in FastAPI

If you decide to use global variables in FastAPI, follow these best practices to mitigate the disadvantages:

  • Use Thread-Safe Data Structures: Use asyncio.Lock, asyncio.Queue, or collections.defaultdict to manage access to global variables and avoid race conditions. -Scope Global Variables Appropriately : Limit the scope of global variables to specific use cases, such as application configuration or one-time initialization values.
  • Consider Dependency Injection: Use FastAPI's dependency injection system to pass shared objects (e.g., database connections, configuration data) to endpoints instead of relying on global variables.
  • Store Persistent Data Externally: Instead of global variables, use external storage solutions like databases, Redis, or message queues to manage shared data.
  • Namespace Globals for Clarity: Use well-structured dictionaries or classes for global data to reduce clutter and prevent accidental overwrites.
  • Use Startup and Shutdown Events: Initialize global variables in FastAPI’s startup event and clean them up in the shutdown event to manage their lifecycle explicitly.

Conclusion

By combining FastAPI with Poetry and following these best practices, you can build scalable, maintainable, and secure applications. From dependency management and environment configuration to exception handling and asynchronous programming, these techniques will ensure your project is ready for production.